package org.farng.mp3.id3;

import org.farng.mp3.AbstractMP3Tag;
import org.farng.mp3.InvalidTagException;
import org.farng.mp3.MP3File;
import org.farng.mp3.TagConstant;
import org.farng.mp3.TagException;
import org.farng.mp3.TagNotFoundException;
import org.farng.mp3.TagOptionSingleton;

import java.io.IOException;
import java.io.RandomAccessFile;

import java.util.HashMap;
import java.util.Iterator;


/**
 * Title:       ID3v2_3 Description: This class represents an ID3v2.30 tag
 * Copyright:   Copyright (c) 2002 Company:
 *
 * @author Eric Farng
 * @version $Revision: 1.7 $
 */
public class ID3v2_3 extends ID3v2_2 {
    /** DOCUMENT ME! */
    protected boolean crcDataFlag = false;

    /** DOCUMENT ME! */
    protected boolean experimental = false;

    /** DOCUMENT ME! */
    protected boolean extended = false;

    /** DOCUMENT ME! */
    protected int crcData = 0;

    /** DOCUMENT ME! */
    protected int paddingSize = 0;

    /**
     * Creates a new ID3v2_3 object.
     */
    public ID3v2_3() {
        majorVersion = 2;
        revision     = 3;
    }

    public ID3v2_3(ID3v2_3 copyObject) {
        super(copyObject);
        this.crcDataFlag  = copyObject.crcDataFlag;
        this.experimental = copyObject.experimental;
        this.extended     = copyObject.extended;
        this.crcData      = copyObject.crcData;
        this.paddingSize  = copyObject.paddingSize;
    }

    public ID3v2_3(AbstractMP3Tag mp3tag) {
        if (mp3tag != null) {
            ID3v2_4 convertedTag;

            if ((mp3tag instanceof ID3v2_4 == false) && (mp3tag instanceof ID3v2_3 == true)) {
                throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument");
            }

            if (mp3tag instanceof ID3v2_4) {
                convertedTag = (ID3v2_4) mp3tag;
            } else {
                convertedTag = new ID3v2_4(mp3tag);
            }

            this.extended          = convertedTag.extended;
            this.experimental      = convertedTag.experimental;
            this.crcDataFlag       = convertedTag.crcDataFlag;
            this.crcData           = convertedTag.crcData;
            this.paddingSize       = convertedTag.paddingSize;
            this.compression       = convertedTag.compression;
            this.unsynchronization = convertedTag.unsynchronization;

            AbstractID3v2      id3tag = (AbstractID3v2) convertedTag;
            Iterator           iterator = id3tag.frameMap.values().iterator();
            AbstractID3v2Frame frame;
            ID3v2_3Frame       newFrame;

            while (iterator.hasNext()) {
                frame    = (AbstractID3v2Frame) iterator.next();
                newFrame = new ID3v2_3Frame(frame);
                frameMap.put(newFrame.getIdentifier(), newFrame);
            }
        }
    }

    /**
     * Creates a new ID3v2_3 object.
     *
     * @param file DOCUMENT ME!
     *
     * @throws TagException DOCUMENT ME!
     * @throws IOException DOCUMENT ME!
     */
    public ID3v2_3(RandomAccessFile file)
            throws TagException, IOException {
        this.read(file);
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public String getIdentifier() {
        return "ID3v2.30";
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public int getSize() {
        int size = 3 + 2 + 1 + 4;

        if (extended) {
            if (crcDataFlag) {
                size += (4 + 2 + 4 + 4);
            } else {
                size += (4 + 2 + 4);
            }
        }

        Iterator           iterator = frameMap.values().iterator();
        AbstractID3v2Frame frame;

        while (iterator.hasNext()) {
            frame = (AbstractID3v2Frame) iterator.next();
            size += frame.getSize();
        }

        return size;
    }

    /**
     * DOCUMENT ME!
     *
     * @param tag DOCUMENT ME!
     */
    public void append(AbstractMP3Tag tag) {
        if (tag instanceof ID3v2_3) {
            this.experimental = ((ID3v2_3) tag).experimental;
            this.extended     = ((ID3v2_3) tag).extended;
            this.crcDataFlag  = ((ID3v2_3) tag).crcDataFlag;
            this.paddingSize  = ((ID3v2_3) tag).paddingSize;
            this.crcData      = ((ID3v2_3) tag).crcData;
        }

        super.append(tag);
    }

    /**
     * DOCUMENT ME!
     *
     * @param obj DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */

//    public void append(RandomAccessFile file)
//                throws IOException, TagException {
//        ID3v2_3 oldTag;
//
//        try {
//            oldTag = new ID3v2_3(file);
//            oldTag.append(this);
//            oldTag.write(file);
//        } catch (TagNotFoundException ex) {
//            oldTag = null;
//        }
//    }

    /**
     * DOCUMENT ME!
     *
     * @param obj DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public boolean equals(Object obj) {
        if ((obj instanceof ID3v2_3) == false) {
            return false;
        }

        ID3v2_3 object = (ID3v2_3) obj;

        if (this.crcData != object.crcData) {
            return false;
        }

        if (this.crcDataFlag != object.crcDataFlag) {
            return false;
        }

        if (this.experimental != object.experimental) {
            return false;
        }

        if (this.extended != object.extended) {
            return false;
        }

        if (this.paddingSize != object.paddingSize) {
            return false;
        }

        return super.equals(obj);
    }

    /**
     * DOCUMENT ME!
     *
     * @param tag DOCUMENT ME!
     */
    public void overwrite(AbstractMP3Tag tag) {
        if (tag instanceof ID3v2_3) {
            this.experimental = ((ID3v2_3) tag).experimental;
            this.extended     = ((ID3v2_3) tag).extended;
            this.crcDataFlag  = ((ID3v2_3) tag).crcDataFlag;
            this.paddingSize  = ((ID3v2_3) tag).paddingSize;
            this.crcData      = ((ID3v2_3) tag).crcData;
        }

        super.overwrite(tag);
    }

    /**
     * DOCUMENT ME!
     *
     * @param file DOCUMENT ME!
     *
     * @throws TagException DOCUMENT ME!
     * @throws IOException DOCUMENT ME!
     * @throws TagNotFoundException DOCUMENT ME!
     * @throws InvalidTagException DOCUMENT ME!
     */

//    public void overwrite(RandomAccessFile file)
//                   throws IOException, TagException {
//        ID3v2_3 oldTag;
//
//        try {
//            oldTag = new ID3v2_3(file);
//            oldTag.overwrite(this);
//            oldTag.write(file);
//        } catch (TagNotFoundException ex) {
//	    super.overwrite(file);
//        }
//    }

    /**
     * DOCUMENT ME!
     *
     * @param file DOCUMENT ME!
     *
     * @throws TagException DOCUMENT ME!
     * @throws IOException DOCUMENT ME!
     * @throws TagNotFoundException DOCUMENT ME!
     * @throws InvalidTagException DOCUMENT ME!
     */
    public void read(RandomAccessFile file)
              throws TagException, IOException {
        int    size;
        byte[] buffer = new byte[4];

        if (seek(file) == false) {
            throw new TagNotFoundException(getIdentifier() + " tag not found");
        }

        // read the major and minor @version number & flags byte
        file.read(buffer, 0, 3);

        if ((buffer[0] != 3) || (buffer[1] != 0)) {
            throw new TagNotFoundException(getIdentifier() + " tag not found");
        }

        majorVersion      = buffer[0];
        revision          = buffer[1];
        unsynchronization = (buffer[2] & TagConstant.MASK_V23_UNSYNCHRONIZATION) != 0;
        extended          = (buffer[2] & TagConstant.MASK_V23_EXTENDED_HEADER) != 0;
        experimental      = (buffer[2] & TagConstant.MASK_V23_EXPERIMENTAL) != 0;

        // read the size
        file.read(buffer, 0, 4);
        size = byteArrayToSize(buffer);

        long filePointer = file.getFilePointer();

        if (extended) {
            // int is 4 bytes.
            int extendedHeaderSize = file.readInt();

            // the extended header is only 6 or 10 bytes.
            if ((extendedHeaderSize != 6) || (extendedHeaderSize != 10)) {
                throw new InvalidTagException("Invalid Extended Header Size.");
            }

            file.read(buffer, 0, 2);
            crcDataFlag = (buffer[0] & TagConstant.MASK_V23_CRC_DATA_PRESENT) != 0;

            // if it's 10 bytes, the CRC flag must be set
            // and if it's 6 bytes, it must not be set
            if (((extendedHeaderSize == 10) && (crcDataFlag == false)) || ((extendedHeaderSize == 6) && (crcDataFlag == true))) {
                throw new InvalidTagException("CRC Data flag not set correctly.");
            }

            paddingSize = file.readInt();

            if ((extendedHeaderSize == 10) && crcDataFlag) {
                crcData = file.readInt();
            }
        }

        ID3v2_3Frame next;
        frameMap = new HashMap();

        // read all the frames.
        this.fileReadSize      = size;
        ID3v2_2.paddingCounter = 0;

        while ((file.getFilePointer() - filePointer) <= size) {
            try {
                next = new ID3v2_3Frame(file);

                String id = next.getIdentifier();

                if (frameMap.containsKey(id)) {
                    this.duplicateFrameId += (id + "; ");
                    this.duplicateBytes += ((AbstractID3v2Frame) frameMap.get(id)).getSize();
                }

                frameMap.put(id, next);

                //long   debugfp        = file.getFilePointer();
                //int    debugpadding   = this.paddingCounter;
                //int    debugsize      = this.getSize();
                //int    debugframesize = next.getSize();
                //int    debugdupe      = this.duplicateBytes;
                //String debugID        = next.getIdentifier();
                //int    j              = 0;
            } catch (InvalidTagException ex) {
                //long   debugfp      = file.getFilePointer();
                //int    debugpadding = this.paddingCounter;
                //int    debugsize    = this.getSize();
                //int    debugtotal   = debugpadding + debugsize;
                //String debugMessage = ex.getMessage();
                if (ex.getMessage().equals("Found empty frame")) {
                    this.emptyFrameBytes += 10;
                } else {
                    this.invalidFrameBytes++;
                }

                if ((file.getFilePointer() - filePointer) <= size) {
                    //System.out.println(ex.getMessage());
                }
            }

            ;
        }

        this.padding = ID3v2_2.paddingCounter;

        /**
         * int newSize = this.getSize(); if ((this.padding + newSize - 10) !=
         * size) { System.out.println("WARNING: Tag sizes don't add up");
         * System.out.println("ID3v2.30 tag size : " + newSize);
         * System.out.println("ID3v2.30 padding  : " + this.padding);
         * System.out.println("ID3v2.30 total    : " + (this.padding +
         * newSize)); System.out.println("ID3v2.30 file size: " + size); }
         */
    }

    /**
     * DOCUMENT ME!
     *
     * @param file DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     *
     * @throws IOException DOCUMENT ME!
     */
    public boolean seek(RandomAccessFile file)
                 throws IOException {
        byte[] buffer = new byte[3];

        file.seek(0);

        // read the tag if it exists
        file.read(buffer, 0, 3);

        String tag = new String(buffer, 0, 3);

        if (tag.equals("ID3") == false) {
            return false;
        }

        // read the major and minor @version number
        file.read(buffer, 0, 2);

        // read back the @version bytes so we can read and save them later
        file.seek(file.getFilePointer() - 2);

        return ((buffer[0] == 3) && (buffer[1] == 0));
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public String toString() {
        Iterator           iterator = frameMap.values().iterator();
        AbstractID3v2Frame frame;
        String             str = getIdentifier() + " " + this.getSize() + "\n";
        str += ("compression        = " + compression + "\n");
        str += ("unsynchronization  = " + unsynchronization + "\n");
        str += ("crcData            = " + crcData + "\n");
        str += ("crcDataFlag        = " + crcDataFlag + "\n");
        str += ("experimental       = " + experimental + "\n");
        str += ("extended           = " + extended + "\n");
        str += ("paddingSize        = " + paddingSize + "\n");

        while (iterator.hasNext()) {
            frame = (ID3v2_3Frame) iterator.next();
            str += (frame.toString() + "\n");
        }

        return str + "\n";
    }

    /**
     * DOCUMENT ME!
     *
     * @param tag DOCUMENT ME!
     */
    public void write(AbstractMP3Tag tag) {
        if (tag instanceof ID3v2_3) {
            this.experimental = ((ID3v2_3) tag).experimental;
            this.extended     = ((ID3v2_3) tag).extended;
            this.crcDataFlag  = ((ID3v2_3) tag).crcDataFlag;
            this.paddingSize  = ((ID3v2_3) tag).paddingSize;
            this.crcData      = ((ID3v2_3) tag).crcData;
        }

        super.write(tag);
    }

    /**
     * DOCUMENT ME!
     *
     * @param file DOCUMENT ME!
     *
     * @throws IOException DOCUMENT ME!
     */
    public void write(RandomAccessFile file)
               throws IOException {
        String   str;
        Iterator iterator;
        byte[]   buffer = new byte[6];

        MP3File  mp3 = new MP3File();
        mp3.seekMP3Frame(file);

        long mp3start = file.getFilePointer();

        if(!TagOptionSingleton.getInstance().isId3v2PaddingCopyTag()){
            // write zero tag
            byte[] zerobuffer = new byte[(int)mp3start];
            file.seek(0);
            file.write(zerobuffer, 0, zerobuffer.length);
        }
        
        file.seek(0);

        ID3v2_3Frame frame;

        str = "ID3";

        for (int i = 0; i < str.length(); i++) {
            buffer[i] = (byte) str.charAt(i);
        }

        buffer[3] = 3;
        buffer[4] = 0;

        if (unsynchronization) {
            buffer[5] |= TagConstant.MASK_V23_UNSYNCHRONIZATION;
        }

        if (extended) {
            buffer[5] |= TagConstant.MASK_V23_EXTENDED_HEADER;
        }

        if (experimental) {
            buffer[5] |= TagConstant.MASK_V23_EXPERIMENTAL;
        }

        file.write(buffer);

        // write size
        file.write(sizeToByteArray((int) mp3start - 10));

        if (extended) {
            if (crcDataFlag) {
                file.writeInt(10);
                buffer[0] = 0;
                buffer[0] |= TagConstant.MASK_V23_CRC_DATA_PRESENT;
                file.write(buffer, 0, 2);
                file.writeInt(paddingSize);
                file.writeInt(crcData);
            } else {
                file.writeInt(6);
                file.write(buffer, 0, 2);
                file.writeInt(paddingSize);
            }
        }

        // write all frames
        iterator = frameMap.values().iterator();

        while (iterator.hasNext()) {
            frame = (ID3v2_3Frame) iterator.next();
            frame.write(file);
        }

        if (this.getSize() != file.getFilePointer()) {
            //System.out.println("ID3v2.20 size didn't match up while writing.");
            //System.out.println("this.getsize()     = " + this.getSize());
            //System.out.println("size (filePointer) = " + file.getFilePointer());
        }
    }
}